package com.lipaluma.parser;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.lipaluma.annotations.mapping.NotMapped;
import com.lipaluma.context.Context;
import com.lipaluma.exceptions.CsvParserException;
import com.lipaluma.field.mapper.FieldMapper;
import com.lipaluma.field.mapper.MarshalFieldMapper;
import com.lipaluma.field.mapper.UnmarshalFieldMapper;
import com.lipaluma.field.mapper.builder.FieldMapperBuilder;
import com.lipaluma.field.mapper.initializer.FieldMapperInitializer;
import com.lipaluma.field.mapper.initializer.XmlFieldMapperInitializer;
import com.lipaluma.field.mapper.mapping.FieldMappingInfo;
import com.lipaluma.io.Row;
import com.lipaluma.io.SourceWriter;
import com.lipaluma.parser.builder.FromParserFieldMapperBuilder;
import com.lipaluma.parser.marshalling.ObjectMarshalled;
import com.lipaluma.util.ReflectionUtils;
import com.lipaluma.xml.XmlMappings;
import com.lipaluma.xml.mapping.XmlFieldMapping;

public abstract class AbstractDataParser<TARGET> implements DataParser<TARGET> {
	private static Logger LOG = LoggerFactory.getLogger(AbstractDataParser.class);

	protected Class<TARGET> targetClass;
	protected List<FieldMapper> fieldMappers;
	protected Context context;
	
	protected AbstractDataParser() {}
	protected AbstractDataParser(AbstractDataParser.Builder<? extends DataParser<TARGET>, TARGET> builder) {
		this.targetClass = builder.targetClass;
		this.context = builder.context;
		this.fieldMappers = new ArrayList<FieldMapper>(builder.mappers);
		initParser();
	}
	protected AbstractDataParser(Class<TARGET> targetClass) {
		this.targetClass = targetClass;
		this.context = createContext();
		this.fieldMappers = declareFields(context, targetClass);
		initParser();
	}
	protected AbstractDataParser(Class<TARGET> targetClass, XmlMappings xmlMappings) {
		this.targetClass = targetClass;
		this.context = createContext();
		this.fieldMappers = declareFields(context, targetClass, xmlMappings.getMappings());
		initParser();
	}
	
	protected abstract void initParser();
		
	protected abstract Context createContext();

	/**
	 * Process a Row of the entry file and returns the object mapped corresponding.
	 * 
	 * @param row a line of the entry file
	 * @return the object mapped
	 */
	protected TARGET unmarshal(Row row) {
		try {
			TARGET target = targetClass.newInstance();
			for (FieldMapper mapper : fieldMappers) {
				if(!UnmarshalFieldMapper.class.isAssignableFrom(mapper.getClass()))
					throw new CsvParserException(mapper.getDescription()+" : the column must be a UnmarshalFieldMapper to parse a row to an object");
				processFieldMapper(target, mapper, row);
			}
			return target;
		} catch (InstantiationException ex) {
			throw new CsvParserException("The Pojo object can't be instanciated.", ex);
		} catch (IllegalAccessException ex) {
			throw new CsvParserException("The Pojo object can't be instanciated.", ex);
		}
	}

	protected void processFieldMapper(TARGET target, FieldMapper fieldMapper , Row row) {
		Object value = fieldMapper.unmarshal(row);
		injectValueToTarget(value, target, fieldMapper.getField());
	}
	
	protected void injectValueToTarget(Object value, TARGET target, Field field) {
		ReflectionUtils.makeAccessible(field);
		if(Collection.class.isAssignableFrom(field.getType())) {
			Collection<Object> collection = (Collection<Object>)ReflectionUtils.getField(field, target);
			if(collection == null) {
				collection = ReflectionUtils.instantiateCollection(field.getType());
				ReflectionUtils.setField(field, target, collection);
			}
			collection.addAll((Collection<Object>)value);
		} else {
			ReflectionUtils.setField(field, target, value);
		}
	}

	
	protected void marshal(TARGET object, SourceWriter writer) {
		ObjectMarshalled objectMarshalled = marshal(object);
		writer.write(objectMarshalled);
	}	
	
	/**
	 * Process an Object and returns a corresponding Row.
	 * 
	 * @param the object
	 * @return the corresponding row
	 */
	protected ObjectMarshalled marshal(Object object) {
		ObjectMarshalled row = new ObjectMarshalled();
		for (FieldMapper fieldMapper : fieldMappers) {
			if(!MarshalFieldMapper.class.isAssignableFrom(fieldMapper.getClass()))
				throw new CsvParserException(fieldMapper.getDescription()+" : the column must be a OneToManyColumnSourceWriter to parse an object to a row");
			if(object != null) {
				Object value = extractValueFromObject(object, fieldMapper.getField());
				injectValueToRow(value, fieldMapper, row);
			}
		}
		return row;
	}
	
	protected Object extractValueFromObject(Object object, Field field) {
		ReflectionUtils.makeAccessible(field);
		return ReflectionUtils.getField(field, object);
	}

	protected void injectValueToRow(Object value, FieldMapper fieldMapper, ObjectMarshalled row) {
		((MarshalFieldMapper)fieldMapper).marshalValue(value, row);
	}
	
	/**
	 * declare the columns by annotations for each fields of the targetEntity except the field annotated {@link NotMapped}
	 * @param clazz the target class to analyse
	 * @return the list of the columns representing each fields not ignored
	 */
	protected List<FieldMapper> declareFields(Context context, Class<TARGET> clazz) {
		List<FieldMapper> fieldMappers = new ArrayList<FieldMapper>();
		Field[] declaredFields = clazz.getDeclaredFields();
		for (Field field : declaredFields) {
			// petit patch pour faire fonctionner eclemma qui ajoute ce nouvel attribut dans les classes pour gérer la couverture
			if(field.getName().equals("$jacocoData"))
				continue;
			if (field.isAnnotationPresent(NotMapped.class))
				continue;
			FieldMapper mapper = createFieldMapper(context, field);
			if(FieldMapperInitializer.class.isAssignableFrom(mapper.getClass()))
				((FieldMapperInitializer)mapper).init(context, field);
			LOG.debug("Type of column "+mapper+" created for : "+field.getName()+" ("+field.getType()+")");
			fieldMappers.add(mapper);
		}
		return fieldMappers;
	}

	protected FieldMapper createFieldMapper(Context context, Field field) {
		return context.getMappingManagerByAnnotation().createFieldMapper(field);
	}

	/**
	 * declare the columns by xml configuration file
	 * @param context TODO
	 * @param clazz target class
	 * @param xmldoc Node root of the xml configuration File
	 * @return the list of the columns representing each columns defined int the xml file
	 */
	protected List<FieldMapper> declareFields(Context context, Class<TARGET> clazz, List<? extends XmlFieldMapping> mappings) {
		List<FieldMapper> fieldMappers = new ArrayList<FieldMapper>();
		for (XmlFieldMapping mapping : mappings) {
			String fieldName = mapping.getField();
			Field field = ReflectionUtils.findField(clazz, fieldName);
			if(field == null)
				throw new CsvParserException("Error on mapping field "+fieldName+". The field is not found on the target entity "+clazz);
			FieldMapper mapper = createFieldMapper(context, field, mapping);
			if(XmlFieldMapperInitializer.class.isAssignableFrom(mapper.getClass()))
				((XmlFieldMapperInitializer)mapper).init(context, field, mapping);
			fieldMappers.add(mapper);
		}
		return fieldMappers;
	}
	
	protected FieldMapper createFieldMapper(Context context, Field field, XmlFieldMapping mapping) {
		return context.getMappingManagerByXml().createFieldMapper(field, mapping);
	}

//	public void addValidator(String fieldName, Validator validator) {
//	for (FieldMapper column : fieldMappers) {
//		if(column.getField().getName().equalsIgnoreCase(fieldName))
//			if(FieldValidated.class.isAssignableFrom(column.getClass()))
//				((FieldValidated)column).addNewValidator(validator);
//			else
//				throw new CsvParserException(column.getDescription()+" : the column doesn't support adding dynamically new validators");
//	}
//}
//
//public void addValidators(String fieldName, Validator... validators) {
//	if(validators == null)
//		throw new CsvParserException("Error adding validators for field name '"+fieldName+"' : validators is null");
//	for (FieldMapper column : fieldMappers) {
//		if(column.getField().getName().equalsIgnoreCase(fieldName))
//			if(FieldValidated.class.isAssignableFrom(column.getClass())) {
//				for (Validator validator : validators) {
//					((FieldValidated)column).addNewValidator(validator);
//				}
//			}
//			else
//				throw new CsvParserException(column.getDescription()+" : the column doesn't support adding dynamically new validators");
//	}
//}
//
//public void addNewValidatorOnEachValue(String fieldName, Validator validator) {
//	for (FieldMapper column : fieldMappers) {
//		if(column.getField().getName().equalsIgnoreCase(fieldName))
//			if(FieldValidatedOnEachValue.class.isAssignableFrom(column.getClass()))
//				((FieldValidatedOnEachValue)column).addNewValidatorOnEachValue(validator);
//			else
//				throw new CsvParserException(column.getDescription()+" : the column doesn't support adding dynamically new validators");
//	}
//}
//
//public void addNewAdaptor(String fieldName, Adaptor adaptor) {
//	for (FieldMapper column : fieldMappers) {
//		if(column.getField().getName().equalsIgnoreCase(fieldName))
//			if(FieldAdapted.class.isAssignableFrom(column.getClass()))
//				((FieldAdapted)column).addNewAdaptor(adaptor);
//			else
//				throw new CsvParserException(column.getDescription()+" : the column doesn't support adding dynamically new adaptors");
//	}
//}
//
//public void addNewAdaptorOnEachValue(String fieldName, Adaptor adaptor) {
//	for (FieldMapper column : fieldMappers) {
//		if(column.getField().getName().equalsIgnoreCase(fieldName))
//			if(FieldAdaptedOnEachValue.class.isAssignableFrom(column.getClass()))
//				((FieldAdaptedOnEachValue)column).addNewAdaptorOnEachValue(adaptor);
//			else
//				throw new CsvParserException(column.getDescription()+" : the column doesn't support adding dynamically new adaptors");
//	}
//}

	public static abstract class Builder<PARSER extends DataParser<TARGET>, TARGET> {
		protected Class<TARGET> targetClass;
		protected List<FieldMapper> mappers = new ArrayList<FieldMapper>();
		protected Context context;
		
		protected Builder(Class<TARGET> targetClass, Context context) {
			this.targetClass = targetClass;
			this.context = context;
		}

		public FromParserFieldMapperBuilder<? extends Builder<PARSER, TARGET>, TARGET> declareFields() {
			return FromParserFieldMapperBuilder.create(this, this.targetClass);
		}

		public Builder<PARSER, TARGET> withFieldMappers(List<FieldMapper> mappers) {
			this.mappers.addAll(mappers);
			return this;
		}
		
		public abstract PARSER build();
	}
}
